﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace TvdP
{
    public static partial class Adapters
    {
        /// <summary>
        /// Class that finds adaptation paths from a given 'fromType' and a collection of adaption methods
        /// to any requested type. It is not guaranteed that such a path can be found.
        /// 
        /// It does this iteratively. An iteration starts with a collection of 'reachable types'. These types
        /// are compared with the input requirements of the adaption methods. Any method that has its input requirements
        /// sattisfied can add a new type to the collection of 'reachable types' in this iteration.
        /// This process can continue untill during some iteration no new types are added to de rachable types collection.
        /// The process starts with just the 'fromType' and it's base types in the 'reachable types' collection (iteration 0).
        /// </summary>
        internal class AdaptationPathFinder
        {
            /// <summary>
            /// A step in an adaptation path. 
            /// </summary>
            public class Step
            {
                public Delegate Delegate { get; set; }
                public Type[] ArgumentTypes { get; set; }
                public Type ReturnType { get; set; }
            }

            class ReachInfo : Step
            {
                /// <summary>
                /// The iteration to reach
                /// the ReturnType from the given fromType using this step
                /// </summary>
                public int Iteration { get; set; }
            }

            class AdaptMethodInfo : Step
            {
                /// <summary>
                /// The number of argument types that still need to be reached before this step can be applied
                /// </summary>
                public int NumberOfDistinctArgumentTypesStillToFind { get; set; }
            }


            /// <summary>
            /// Comparer to determine the relative priority of alternative paths
            /// </summary>
            class ReachInfoComparerImplementation : IComparer<ReachInfo>
            {
                public ReachInfoComparerImplementation(IDictionary<Type, ReachInfo> reachableTypes)
                { ReachableTypes = reachableTypes; }

                IDictionary<Type, ReachInfo> ReachableTypes;

                static int? Option(int c)
                { return c == 0 ? (int?)null : c; }

                public int Compare(ReachInfo x, ReachInfo y)
                {
                    return
                        x.Delegate == null ? -1 :   //safeguard.. comparing Reachinfos of level 0
                        y.Delegate == null ? 1 :
                        (
                            Option(Comparer<int>.Default.Compare(x.Iteration, y.Iteration))                 //earlier iteration always first
                            ?? Option(Adapters.AdapterComparer._Instance.Compare(x.Delegate, y.Delegate))   //weighing of the delegate signatures
                            ?? Option(Comparer<int>.Default.Compare(Weight(x), Weight(y)))                  //route with smallest number of total adaptations first
                            ?? Comparer<string>.Default.Compare(HeavyName(x), HeavyName(y))                 //last resort arbitrarily by name.
                        )
                    ;
                }

                public string HeavyName(ReachInfo reachInfo)
                {
                    var method = reachInfo.Delegate.Method;

                    return method.Name + "." + method.DeclaringType.FullName;
                }

                public int Weight(ReachInfo reachInfo)
                { return WeightRec(reachInfo, new HashSet<Type>()); }

                public int WeightRec(ReachInfo reachInfo, HashSet<Type> visited)
                {
                    if (reachInfo.Iteration == 0 || visited.Contains(reachInfo.ReturnType))
                        return 0;

                    visited.Add(reachInfo.ReturnType);

                    int weight = 1;

                    foreach (var argReach in reachInfo.ArgumentTypes.Select(at => ReachableTypes[at]))
                        weight += WeightRec(argReach, visited);

                    return weight;
                }
            }

            /// <summary>
            /// Creates an adaption path finder to find paths from a given <paramref name="fromType"/> using adaption delegates given in <paramref name="adaptMethods"/>
            /// </summary>
            /// <param name="fromType">The type to find adaption paths from.</param>
            /// <param name="adaptMethods">The delegates to construct adaption paths with.</param>
            /// <remarks>
            /// All adaptMethods delegate must match certain signature criteria. These are tested using the method Adapter.VerifiedThatRequiredSignatureIsMatched
            /// </remarks>
            public AdaptationPathFinder(Type fromType, IEnumerable<Delegate> adaptMethods)
            {
                SpecificArgumentTypeToAdaptMethodMap = BuildSpecificArgumentTypeToAdaptMethodMap(adaptMethods);

                ReachableSpecificTypes = new Dictionary<Type, ReachInfo>();
                ReachInfoComparer = new ReachInfoComparerImplementation(ReachableSpecificTypes);

                CurrentIteration = 0;
                NoMoreTypesToReach = false;

                LastIterationAddedReachableSpecificTypes = new Dictionary<Type, ReachInfo>();
                TryAddTypesToReachableTypesCollection(
                    new[] { 
                        new ReachInfo { 
                            Iteration= CurrentIteration, 
                            Delegate = null, 
                            ReturnType = fromType, 
                            ArgumentTypes = new[] { fromType } 
                        } 
                    }
                );
            }

            //Comparer used to determine which path to use when multiple paths lead to the same specific type.
            IComparer<ReachInfo> ReachInfoComparer;

            //All specific types reachable within the current number of iterations (steps)
            IDictionary<Type, ReachInfo> ReachableSpecificTypes;

            //All new specific types added during the last iteration
            //used to determine which types can be reached with the next iteration
            //(equivalent to ReachableSpecificTypes where reachInfo.Iteration == CurrentIteration)
            IDictionary<Type, ReachInfo> LastIterationAddedReachableSpecificTypes;

            //The current maximum number of steps that can be used to reach types stored in ReachableSpecificTypes
            //Each iteration adds a next step to the paths and therefore types to the reachable types collection
            int CurrentIteration;

            //The last iteration did not add any new reachable types. This means that no new types can be reached in any further iterations
            //and investigating any further iterations is pointless. Any types not in the ReachableSpecificTypes collection can
            //not be reached using the current collection of adapters.
            bool NoMoreTypesToReach;

            //Map of specific argument type to adapt method. When all argument types of a method can be reached, the method
            //can be used to reach a new type in the next iteration.
            ILookup<Type, AdaptMethodInfo> SpecificArgumentTypeToAdaptMethodMap;

            ILookup<Type, AdaptMethodInfo> BuildSpecificArgumentTypeToAdaptMethodMap(IEnumerable<Delegate> adaptMethods)
            {
                return
                    adaptMethods
                        .SelectMany(
                            dgt =>
                            {
                                var argumentTypes = dgt.Method.GetParameters().Select(p => p.ParameterType).ToArray();
                                var distinctArgumentTypes = argumentTypes.Distinct().ToArray();

                                var adaptMethodInfo =
                                    new AdaptMethodInfo
                                    {
                                        Delegate = dgt,
                                        ArgumentTypes = argumentTypes,
                                        ReturnType = dgt.Method.ReturnType,
                                        NumberOfDistinctArgumentTypesStillToFind = distinctArgumentTypes.Length
                                    }
                                ;

                                return
                                    distinctArgumentTypes.Select(argType => new { argType, adaptMethodInfo })
                               ;
                            }
                        )
                        .ToArray()
                        .ToLookup(set => set.argType, set => set.adaptMethodInfo)
               ;
            }

            /// <summary>
            /// Use the reachable types newly added during the previous iteration to update the NumberOfDistinctArgumentTypesStillToFind counts in the SpecificArgumentTypeToAdaptMethodMap.
            /// All adapt methods that reach count == 0 are candidates to provide new reachable types in this iteration.
            /// </summary>
            /// <param name="previousIterationAdditions"></param>
            /// <param name="iteration"></param>
            /// <returns></returns>
            IEnumerable<ReachInfo> GetNextIterationPotentialPaths()
            {
                var list = new List<ReachInfo>();

                //note: not linq statement. Modifying NumberOfDistinctArgumentTypesStillToFind and therefore not pure.
                foreach (var addition in LastIterationAddedReachableSpecificTypes)
                    foreach (var methodInfo in SpecificArgumentTypeToAdaptMethodMap[addition.Key])
                        if (--methodInfo.NumberOfDistinctArgumentTypesStillToFind == 0)
                            list.Add(
                                new ReachInfo { 
                                    Iteration = CurrentIteration, 
                                    Delegate = methodInfo.Delegate, 
                                    ArgumentTypes = methodInfo.ArgumentTypes, 
                                    ReturnType = methodInfo.ReturnType 
                                }
                            );

                return list;
            }

            /// <summary>
            /// Get direct base types of the given type.
            /// </summary>
            /// <param name="type"></param>
            /// <returns></returns>
            /// <remarks>
            /// Type object is given as base type for interfaces too.
            /// </remarks>
            IEnumerable<Type> DirectBaseTypesGetter(Type type)
            {
                if (type != typeof(object))
                {
                    yield return type.BaseType ?? typeof(object);

                    foreach (var interfaceType in type.GetInterfaces())
                        yield return interfaceType;
                }
            }

            /// <summary>
            /// Tries to add a specific type to the ReachableSpecificTypes collection.
            /// </summary>
            /// <param name="type"></param>
            /// <param name="reachableInfo"></param>
            /// <returns></returns>
            /// <remarks>
            /// The type may not be added if it is already present in de collection and it's reachable info is prefered over the new reachable info. It may also be replaced if a later addition
            /// of the same type has a reachable info with preference over this one.
            /// </remarks>
            bool TryAddSpecificTypeToReachableTypesCollection(Type type, ReachInfo reachableInfo)
            {
                ReachInfo competingReachableInfo;

                if (ReachableSpecificTypes.TryGetValue(type, out competingReachableInfo) && ReachInfoComparer.Compare(competingReachableInfo, reachableInfo) <= 0)
                    return false;
                else
                {
                    ReachableSpecificTypes[type] = reachableInfo;
                    LastIterationAddedReachableSpecificTypes[type] = reachableInfo;

                    return true;
                }
            }

            /// <summary>
            /// Try add a type and its base types to the ReachableSpecificTypes collection.
            /// </summary>
            /// <param name="type"></param>
            /// <param name="reachableInfo"></param>
            /// <remarks>
            /// The entire hierarchy, only part or none at all may be added to the collection.
            /// </remarks>
            void TryAddTypeToReachableTypesCollection(Type type, ReachInfo reachableInfo)
            {
                if (TryAddSpecificTypeToReachableTypesCollection(type, reachableInfo))
                    foreach (Type baseType in DirectBaseTypesGetter(type))
                        TryAddTypeToReachableTypesCollection(baseType, reachableInfo);
            }

            void TryAddTypesToReachableTypesCollection(IEnumerable<ReachInfo> newReachableTypes)
            {
                foreach (var reachInfo in newReachableTypes)
                    TryAddTypeToReachableTypesCollection(reachInfo.ReturnType, reachInfo);
            }

            List<ReachInfo> CollectPathTo(Type specificType, HashSet<Type> visited)
            {
                var reachInfo = ReachableSpecificTypes[specificType];

                if (visited.Contains(reachInfo.ReturnType))
                    return new List<ReachInfo>();

                visited.Add(reachInfo.ReturnType);

                //new reachinfo with argument types converted to actual types found in path (actual type may be a derived type of the argument type)
                var newReachInfo =
                    new ReachInfo
                    {
                        Delegate = reachInfo.Delegate,
                        ReturnType = reachInfo.ReturnType,
                        Iteration = reachInfo.Iteration,
                        ArgumentTypes = reachInfo.ArgumentTypes.Select(argt => ReachableSpecificTypes[argt].ReturnType).ToArray()
                    }
                ;

                var collectedPathsToArguments = new List<IEnumerable<ReachInfo>>();

                //note: not in linq statement. We use and modify the visited collection while traversing. That makes it not pure.
                foreach (var argType in newReachInfo.ArgumentTypes)
                    collectedPathsToArguments.Add(CollectPathTo(argType, visited));

                return
                    new[] { 
                        newReachInfo 
                    }
                    .Concat( collectedPathsToArguments.SelectMany(paths => paths) )
                    .ToList()
                ;
            }

            bool TypeIsReachable(Type toType)
            {
                while (!ReachableSpecificTypes.ContainsKey(toType))
                {
                    if (NoMoreTypesToReach)
                        return false;

                    ++CurrentIteration;
                    var nextIterationPotentialPaths = GetNextIterationPotentialPaths();
                    LastIterationAddedReachableSpecificTypes = new Dictionary<Type, ReachInfo>();
                    TryAddTypesToReachableTypesCollection(nextIterationPotentialPaths);
                    NoMoreTypesToReach = !LastIterationAddedReachableSpecificTypes.Any();
                }

                return true;
            }

            /// <summary>
            ///Tries to find an adaptation path to <paramref name="toType"/>
            /// </summary>
            /// <param name="toType">The type to find a path to.</param>
            /// <returns>Sequence of adapt methods that will lead to the requested type or null no such path can be found.</returns>
            /// <remarks>
            /// If the sequence is empty then <paramref name="toType"/> is equal to or a base type of 'fromType'.
            /// The first Step in the resulting sequence has at least one input argument type which will be equal to or a base type of 'fromType'.
            /// The last Step int the resulting sequence will have a result type that is equal to or a derived type of <paramref name="toType"/>.
            /// </remarks>
            public IEnumerable<Step> FindPathTo(Type toType)
            {
                if (TypeIsReachable(toType))
                {
                    var unorderedSteps = CollectPathTo(toType, new HashSet<Type>());

                    var path =
                        unorderedSteps
                            .Where(ri => ri.Iteration != 0)     //leave out step from iteration 0, which is not realy a step.
                            .OrderBy(ri => ri.Iteration)        //In this way all dependencies come before dependents.
                            .Select(
                                ri => new Step
                                {
                                    Delegate = ri.Delegate,
                                    ArgumentTypes = ri.ArgumentTypes,
                                    ReturnType = ri.ReturnType
                                }
                            )
                            .ToList()
                    ;

                    return path;
                }
                else
                    return null;
            }
        }

    }
}
